using System.Runtime.CompilerServices;
using System.Collections.Generic;

//
// * Copyright (c) 2002-2009 "Neo Technology,"
// *     Network Engine for Objects in Lund AB [http://neotechnology.com]
// *
// * This file is part of Neo4j.
// * 
// * Neo4j is free software: you can redistribute it and/or modify
// * it under the terms of the GNU Affero General Public License as
// * published by the Free Software Foundation, either version 3 of the
// * License, or (at your option) any later version.
// * 
// * This program is distributed in the hope that it will be useful,
// * but WITHOUT ANY WARRANTY; without even the implied warranty of
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// * GNU Affero General Public License for more details.
// * 
// * You should have received a copy of the GNU Affero General Public License
// * along with this program. If not, see <http://www.gnu.org/licenses/>.
// 
namespace org.neo4j.kernel.impl.transaction
{


	using Xid = javax.transaction.xa.Xid;

// TODO: fixed sized logs (pre-initialize them)
// keep dangling records in memory for log switch
// batch disk forces
///
/// <summary> * This class is made public for testing purposes only, do not use.
/// * <p>
/// * The <seealso cref="TxManager"/> uses this class to keep a transaction log for
/// * transaction recovery.
/// * <p> </summary>
/// 
	public class TxLog
	{
		private string name = null;
		private FileChannel fileChannel = null;
		private ByteBuffer buffer = null;
		private int recordCount = 0;

		public const sbyte TX_START = 1;
		public const sbyte BRANCH_ADD = 2;
		public const sbyte MARK_COMMIT = 3;
		public const sbyte TX_DONE = 4;

///    
///     <summary> * Initializes a transaction log using <CODE>filename</CODE>. If the file
///     * isn't empty the position will be set to size of file so new records will
///     * be appended.
///     *  </summary>
///     * <param name="fileName">
///     *            Filename of file to use </param>
///     * <exception cref="IOException">
///     *             If unable to open file </exception>
///     
//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: public TxLog(String fileName) throws IOException
		public TxLog(string fileName)
		{
			if (fileName == null)
			{
				throw new IllegalArgumentException("Null filename");
			}
			fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
			fileChannel.position(fileChannel.size());
			buffer = ByteBuffer.allocateDirect((3 + Xid.MAXGTRIDSIZE + Xid.MAXBQUALSIZE) * 1000);
			this.name = fileName;
		}

///    
///     <summary> * Returns the name of the transaction log. </summary>
///     
		public virtual string Name
		{
			get
			{
				return name;
			}
		}

///    
///     <summary> * Returns the number of records (one of TX_START,BRANCH_ADD,MARK_COMMIT or
///     * TX_DONE) written since this instance was created or truncated. </summary>
///     
		public virtual int getRecordCount()
		{
			return recordCount;
		}

///    
///     <summary> * Closes the file representing the transaction log. </summary>
///     
//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: public void close() throws IOException
		public virtual void close()
		{
			fileChannel.Close();
		}

///    
///     <summary> * Forces the log file (with metadata). Useful when switching log. </summary>
///     
//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: public void force() throws IOException
		public virtual void force()
		{
			fileChannel.force(true);
		}

///    
///     <summary> * Truncates the file to zero size and sets the record count to zero. </summary>
///     
//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: public synchronized void truncate() throws IOException
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual void truncate()
		{
			fileChannel.position(0);
			fileChannel.truncate(0);
			recordCount = 0;
		}

///    
///     <summary> * Writes a <CODE>TX_START</CODE> record to the file.
///     *  </summary>
///     * <param name="globalId">
///     *            The global id of the new transaction </param>
///     * <exception cref="IOException">
///     *             If unable to write </exception>
///     
	// tx_start(byte)|gid_length(byte)|globalId
//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: public synchronized void txStart(byte globalId[]) throws IOException
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual void txStart(sbyte[] globalId)
		{
			if (globalId == null)
			{
				throw new IllegalArgumentException("Null parameter");
			}
			buffer.Clear();
			buffer.put(TX_START).put((sbyte) globalId.Length).put(globalId);
			buffer.flip();
			fileChannel.write(buffer);
			recordCount++;
		}

///    
///     <summary> * Writes a <CODE>BRANCH_ADD</CODE> record to the file.
///     *  </summary>
///     * <param name="globalId">
///     *            The global id of the transaction </param>
///     * <param name="branchId">
///     *            The branch id for the enlisted resource </param>
///     * <exception cref="IOException">
///     *             If unable to write </exception>
///     
	// add_branch(byte)|gid_length(byte)|bid_length(byte)|globalId|branchId
//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: public synchronized void addBranch(byte globalId[] , byte branchId[]) throws IOException
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual void addBranch(sbyte[] globalId, sbyte[] branchId)
		{
			if (globalId == null)
			{
				throw new IllegalArgumentException("Null global id");
			}
			if (branchId == null)
			{
				throw new IllegalArgumentException("Null branch id");
			}
			buffer.Clear();
			buffer.put(BRANCH_ADD).put((sbyte) globalId.Length).put((sbyte) branchId.Length).put(globalId).put(branchId);
			buffer.flip();
			fileChannel.write(buffer);
			recordCount++;
		}

///    
///     <summary> * Writes a <CODE>MARK_COMMIT</CODE> record to the file and forces the
///     * file to disk.
///     *  </summary>
///     * <param name="globalId">
///     *            The global id of the transaction </param>
///     * <exception cref="IOException">
///     *             If unable to write </exception>
///     
	// mark_committing(byte)|gid_length(byte)|globalId
	// forces
//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: public synchronized void markAsCommitting(byte globalId[]) throws IOException
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual void markAsCommitting(sbyte[] globalId)
		{
			if (globalId == null)
			{
				throw new IllegalArgumentException("Null parameter");
			}
			buffer.Clear();
			buffer.put(MARK_COMMIT).put((sbyte) globalId.Length).put(globalId);
			buffer.flip();
			fileChannel.write(buffer);
			fileChannel.force(false);
			recordCount++;
		}

///    
///     <summary> * Writes a <CODE>TX_DONE</CODE> record to the file.
///     *  </summary>
///     * <param name="globalId">
///     *            The global id of the transaction completed </param>
///     * <exception cref="IOException">
///     *             If unable to write </exception>
///     
	// tx_done(byte)|gid_length(byte)|globalId
//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: public synchronized void txDone(byte globalId[]) throws IOException
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual void txDone(sbyte[] globalId)
		{
			if (globalId == null)
			{
				throw new IllegalArgumentException("Null parameter");
			}
			buffer.Clear();
			buffer.put(TX_DONE).put((sbyte) globalId.Length).put(globalId);
			buffer.flip();
			fileChannel.write(buffer);
			recordCount++;
		}

///    
///     <summary> * Made public for testing only.
///     * <p>
///     * Wraps a transaction record in the tx log file. </summary>
///     
		public class Record
		{
			private sbyte type = 0;
			private sbyte[] globalId = null;
			private sbyte[] branchId = null;
			private int seqNr = -1;

			internal Record(sbyte type, sbyte[] globalId, sbyte[] branchId, int seqNr)
			{
				if (type < 1 || type > 4)
				{
					throw new IllegalArgumentException("Illegal type: " + type);
				}
				this.type = type;
				this.globalId = globalId;
				this.branchId = branchId;
				this.seqNr = seqNr;
			}

			public virtual sbyte getType()
			{
				return type;
			}

			public virtual sbyte[] getGlobalId()
			{
				return globalId;
			}

			public virtual sbyte[] getBranchId()
			{
				return branchId;
			}

			public virtual int getSequenceNumber()
			{
				return seqNr;
			}

			public override string ToString()
			{
				XidImpl xid = new XidImpl(globalId, branchId == null ? new sbyte[0] : branchId);
				return "TxLogRecord[" + type + "," + xid + "," + seqNr + "]";
			}
		}

//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: void writeRecord(Record record) throws IOException
		internal virtual void writeRecord(Record record)
		{
			if (record.getType() == TX_START)
			{
				txStart(record.getGlobalId());
			}
			else if (record.getType() == BRANCH_ADD)
			{
				addBranch(record.getGlobalId(), record.getBranchId());
			}
			else if (record.getType() == MARK_COMMIT)
			{
				markAsCommitting(record.getGlobalId());
			}
			else
			{
			// TX_DONE should never be passed in here
				throw new IOException("Illegal record type[" + record.getType() + "]");
			}
		}

///    
///     <summary> * Returns an array of lists, each list contains dangling records
///     * (transactions that han't been completed yet) grouped after global by
///     * transaction id. </summary>
///     
//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: public synchronized IEnumerator <List<Record>> getDanglingRecords() throws IOException
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual IEnumerator <List<Record>> getDanglingRecords()
		{
			fileChannel.position(0);
			buffer.Clear();
			fileChannel.read(buffer);
			buffer.flip();
		// next record position
			long nextPosition = 0;
		// holds possible dangling records
			int seqNr = 0;
			Map<Xid, List<Record>> recordMap = new Dictionary<Xid, List<Record>>();
			while (buffer.hasRemaining())
			{
				sbyte recordType = buffer.get();
				if (recordType == TX_START)
				{
					if (!buffer.hasRemaining())
					{
						break;
					}
					sbyte[] globalId = new sbyte[buffer.get()];
					if (buffer.limit() - buffer.position() < globalId.Length)
					{
						break;
					}
					buffer.get(globalId);
					Xid xid = new XidImpl(globalId, new sbyte[0]);
					if (recordMap.containsKey(xid))
					{
						throw new IOException("Tx start for same xid[" + xid + "] found twice");
					}
					List<Record> recordList = new LinkedList<Record>();
					recordList.Add(new Record(recordType, globalId, null, seqNr++));
					recordMap.put(xid, recordList);
					nextPosition += 2 + globalId.Length;
				}
				else if (recordType == BRANCH_ADD)
				{
					if (buffer.limit() - buffer.position() < 2)
					{
						break;
					}
					sbyte[] globalId = new sbyte[buffer.get()];
					sbyte[] branchId = new sbyte[buffer.get()];
					if (buffer.limit() - buffer.position() < globalId.Length + branchId.Length)
					{
						break;
					}
					buffer.get(globalId);
					buffer.get(branchId);
					Xid xid = new XidImpl(globalId, new sbyte[0]);
					if (!recordMap.containsKey(xid))
					{
						throw new IOException("Branch[" + new string(branchId) + "] found for [" + xid + "] but no record list found in map");
					}
					recordMap.get(xid).Add(new Record(recordType, globalId, branchId, seqNr++));
					nextPosition += 3 + globalId.Length + branchId.Length;
				}
				else if (recordType == MARK_COMMIT)
				{
					if (!buffer.hasRemaining())
					{
						break;
					}
					sbyte[] globalId = new sbyte[buffer.get()];
					if (buffer.limit() - buffer.position() < globalId.Length)
					{
						break;
					}
					buffer.get(globalId);
					Xid xid = new XidImpl(globalId, new sbyte[0]);
					if (!recordMap.containsKey(xid))
					{
						throw new IOException("Commiting xid[" + xid + "] mark found but no record list found in map");
					}
					List<Record> recordList = recordMap.get(xid);
					recordList.Add(new Record(recordType, globalId, null, seqNr++));
					recordMap.put(xid, recordList);
					nextPosition += 2 + globalId.Length;
				}
				else if (recordType == TX_DONE)
				{
					if (!buffer.hasRemaining())
					{
						break;
					}
					sbyte[] globalId = new sbyte[buffer.get()];
					if (buffer.limit() - buffer.position() < globalId.Length)
					{
						break;
					}
					buffer.get(globalId);
					Xid xid = new XidImpl(globalId, new sbyte[0]);
					if (!recordMap.containsKey(xid))
					{
						throw new IOException("Commiting xid[" + xid + "] mark found but no record list found in map");
					}
					recordMap.Remove(xid);
					nextPosition += 2 + globalId.Length;
				}
				else if (recordType == 0)
				{
					continue;
				}
				else
				{
					throw new IOException("Unknown type: " + recordType);
				}
				if ((buffer.limit() - buffer.position()) < (3 + Xid.MAXGTRIDSIZE + Xid.MAXBQUALSIZE))
				{
				// make sure we don't try to read non full entry
					buffer.Clear();
					fileChannel.position(nextPosition);
					fileChannel.read(buffer);
					buffer.flip();
				}
			}
			return recordMap.values().GetEnumerator();
		}

///    
///     <summary> * Switches log file. Copies the dangling records in current log file to the
///     * <CODE>newFile</CODE> and the makes the switch closing the old log file.
///     *  </summary>
///     * <param name="newFile">
///     *            The filename of the new file to switch to </param>
///     * <exception cref="IOException">
///     *             If unable to switch log file </exception>
///     
//JAVA TO VB & C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
//ORIGINAL LINE: public synchronized void switchToLogFile(String newFile) throws IOException
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual void switchToLogFile(string newFile)
		{
			if (newFile == null)
			{
				throw new IllegalArgumentException("Null filename");
			}
		// copy all dangling records from current log to new log
			force();
			IEnumerator <List<Record>> itr = getDanglingRecords();
			Close();
			List<Record> records = new List<Record>();
			while (itr.MoveNext())
			{
				records.AddRange(itr.Current);
			}
			Collections.sort(records, new Comparator<Record>() { public int compare(Record r1, Record r2) { return r1.getSequenceNumber() - r2.getSequenceNumber(); } });
			List<Record>.Enumerator recordItr = records.GetEnumerator();
			fileChannel = new RandomAccessFile(newFile, "rw").getChannel();
			fileChannel.position(fileChannel.size());
			name = newFile;
			truncate();
			while (recordItr.MoveNext())
			{
				Record record = recordItr.Current;
				writeRecord(record);
			}
			force();
		}
	}
}